package net.aiion.weave.impl.bytecode;

import java.util.WeakHashMap;

import net.aiion.weave.asm.Attribute;
import net.aiion.weave.asm.Handle;
import net.aiion.weave.asm.Label;
import net.aiion.weave.impl.bytecode.events.AnnotationAnnotationValueBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.AnnotationArrayValueBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.AnnotationDefaultStartBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.AnnotationEndBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.AnnotationEnumValueBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.AnnotationStartBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.AnnotationValueBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.AttributeBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.ClassEndBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.ClassStartBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.CodeEndBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.CodeStartBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.FieldEndBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.FieldInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.FieldStartBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.FrameBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.IincInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.InnerClassBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.InstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.IntInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.InvokeDynamicInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.JumpInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.LabelBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.LineNumberBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.LoadConstantInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.LocalVariableBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.LookupSwitchInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.MethodEndBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.MethodInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.MethodStartBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.MultiANewArrayInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.OuterClassBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.ParameterAnnotationStartBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.SourceBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.TableSwitchInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.TryCatchBlockBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.TypeInstructionBytecodeEventImpl;
import net.aiion.weave.impl.bytecode.events.VariableInstructionBytecodeEventImpl;
import net.aiion.weave.spi.bytecode.BytecodeEventFactory;
import net.aiion.weave.spi.bytecode.events.AnnotationAnnotationValueBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationArrayValueBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationDefaultStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationEnumValueBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AnnotationValueBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.AttributeBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.BytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ClassStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.CodeEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.CodeStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FieldEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FieldInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FieldStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.FrameBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.IincInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.InnerClassBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.InstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.IntInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.InvokeDynamicInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.JumpInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.LabelBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.LineNumberBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.LoadConstantInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.LocalVariableBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.LookupSwitchInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodEndBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MethodStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.MultiANewArrayInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.OuterClassBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.ParameterAnnotationStartBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.SourceBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.TableSwitchInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.TryCatchBlockBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.TypeInstructionBytecodeEvent;
import net.aiion.weave.spi.bytecode.events.VariableInstructionBytecodeEvent;

public class BytecodeEventFactoryImpl implements BytecodeEventFactory
{
    private static final int HASH_PRIME = 31;

    private final WeakHashMap<Integer, BytecodeEvent> cachedBytecodeEvents;

    public BytecodeEventFactoryImpl() {
        cachedBytecodeEvents = new WeakHashMap<>();
    }

    @Override
    public ClassStartBytecodeEvent createClassStart(final int aVersion, final int aModifiers, final String aName,
        final String aSignature, final String aSuperName, final String[] aInterfaceNames)
    {
        return new ClassStartBytecodeEventImpl(aVersion, aModifiers, aName, aSignature, aSuperName, aInterfaceNames);
    }

    @Override
    public SourceBytecodeEvent createSource(final String aSourceFileName, final String aDebugInformation) {
        return new SourceBytecodeEventImpl(aSourceFileName, aDebugInformation);
    }

    @Override
    public OuterClassBytecodeEvent createOuterClass(final String aEnclosingClassName, final String aMethodName,
        final String aMethodDescriptor)
    {
        return new OuterClassBytecodeEventImpl(aEnclosingClassName, aMethodName, aMethodDescriptor);
    }

    @Override
    public AnnotationStartBytecodeEvent createAnnotationStart(final String aDescriptor, final boolean aVisible) {
        return new AnnotationStartBytecodeEventImpl(aDescriptor, aVisible);
    }

    @Override
    public AnnotationValueBytecodeEvent createAnnotationValue(final String aName, final Object aValue) {
        return new AnnotationValueBytecodeEventImpl(aName, aValue);
    }

    @Override
    public AnnotationEnumValueBytecodeEvent createAnnotationEnumValue(final String aName, final String aDescriptor,
        final String aValue)
    {
        return new AnnotationEnumValueBytecodeEventImpl(aName, aDescriptor, aValue);
    }

    @Override
    public AnnotationAnnotationValueBytecodeEvent createAnnotationAnnotationValue(final String aName,
        final String aDescriptor)
    {
        return new AnnotationAnnotationValueBytecodeEventImpl(aName, aDescriptor);
    }

    @Override
    public AnnotationArrayValueBytecodeEvent createAnnotationArrayValue(final String aName) {
        return new AnnotationArrayValueBytecodeEventImpl(aName);
    }

    @Override
    public AnnotationEndBytecodeEvent createAnnotationEnd() {
        return AnnotationEndBytecodeEventImpl.getInstance();
    }

    @Override
    public AttributeBytecodeEvent createAttribute(final Attribute aAttribute) {
        return new AttributeBytecodeEventImpl(aAttribute);
    }

    @Override
    public InnerClassBytecodeEvent createInnerClass(final String aInnerName, final String aOuterName,
        final String aInnerSimpleName, final int aModifiers)
    {
        return new InnerClassBytecodeEventImpl(aInnerName, aOuterName, aInnerSimpleName, aModifiers);
    }

    @Override
    public FieldStartBytecodeEvent createFieldStart(final int aModifiers, final String aName, final String aDescriptor,
        final String aSignature, final Object aConstantValue)
    {
        return new FieldStartBytecodeEventImpl(aModifiers, aName, aDescriptor, aSignature, aConstantValue);
    }

    @Override
    public FieldEndBytecodeEvent createFieldEnd() {
        return FieldEndBytecodeEventImpl.getInstance();
    }

    @Override
    public MethodStartBytecodeEvent createMethodStart(final int aModifiers, final String aName,
        final String aDescriptor, final String aSignature, final String[] aExceptionNames)
    {
        return new MethodStartBytecodeEventImpl(aModifiers, aName, aDescriptor, aSignature, aExceptionNames);
    }

    @Override
    public AnnotationDefaultStartBytecodeEvent createAnnotationDefaultStart() {
        return AnnotationDefaultStartBytecodeEventImpl.getInstance();
    }

    @Override
    public ParameterAnnotationStartBytecodeEvent createParameterAnnotationStart(final int aParameterIndex,
        final String aDescriptor, final boolean aVisible)
    {
        return new ParameterAnnotationStartBytecodeEventImpl(aParameterIndex, aDescriptor, aVisible);
    }

    @Override
    public CodeStartBytecodeEvent createCodeStart() {
        return CodeStartBytecodeEventImpl.getInstance();
    }

    @Override
    public FrameBytecodeEvent createFrame(final int aFrameType, final int aLocalVariables,
        final Object[] aLocalVariableTypes, final int aStackElements, final Object[] aStackElementTypes)
    {
        return new FrameBytecodeEventImpl(aFrameType, aLocalVariables, aLocalVariableTypes, aStackElements,
            aStackElementTypes);
    }

    @Override
    public FieldInstructionBytecodeEvent createFieldInstruction(final int aOpcode, final String aOwnerName,
        final String aFieldName, final String aFieldDescriptor)
    {
        return new FieldInstructionBytecodeEventImpl(aOpcode, aOwnerName, aFieldName, aFieldDescriptor);
    }

    @Override
    public IincInstructionBytecodeEvent createIincInstruction(final int aLocalVariableIndex, final int aIncrement) {
        final int hashCode = hashCode(BytecodeEvent.IINC_INSTRUCTION, aLocalVariableIndex, aIncrement);
        if (cachedBytecodeEvents.containsKey(hashCode)) {
            return (IincInstructionBytecodeEvent) cachedBytecodeEvents.get(hashCode);
        }

        final IincInstructionBytecodeEvent iincInstructionBytecodeEvent = new IincInstructionBytecodeEventImpl(
            aLocalVariableIndex, aIncrement);
        cachedBytecodeEvents.put(hashCode, iincInstructionBytecodeEvent);
        return iincInstructionBytecodeEvent;
    }

    @Override
    public InstructionBytecodeEvent createInstruction(final int aOpcode) {
        final int hashCode = hashCode(BytecodeEvent.INSTRUCTION, aOpcode);
        if (cachedBytecodeEvents.containsKey(hashCode)) {
            return (InstructionBytecodeEvent) cachedBytecodeEvents.get(hashCode);
        }

        final InstructionBytecodeEvent instructionBytecodeEvent = new InstructionBytecodeEventImpl(aOpcode);
        cachedBytecodeEvents.put(hashCode, instructionBytecodeEvent);
        return instructionBytecodeEvent;
    }

    @Override
    public IntInstructionBytecodeEvent createIntInstruction(final int aOpcode, final int aOperand) {
        final int hashCode = hashCode(BytecodeEvent.INSTRUCTION, aOpcode, aOperand);
        if (cachedBytecodeEvents.containsKey(hashCode)) {
            return (IntInstructionBytecodeEvent) cachedBytecodeEvents.get(hashCode);
        }

        final IntInstructionBytecodeEvent intInstructionBytecodeEvent = new IntInstructionBytecodeEventImpl(aOpcode,
            aOperand);
        cachedBytecodeEvents.put(hashCode, intInstructionBytecodeEvent);
        return intInstructionBytecodeEvent;
    }

    @Override
    public InvokeDynamicInstructionBytecodeEvent createInvokeDynamicInstruction(final String aMethodName,
        final String aMethodDescriptor, final Handle aBootstrapMethodHandle, final Object[] aBootstrapMethodArguments)
    {
        return new InvokeDynamicInstructionBytecodeEventImpl(aMethodName, aMethodDescriptor, aBootstrapMethodHandle,
            aBootstrapMethodArguments);
    }

    @Override
    public JumpInstructionBytecodeEvent createJumpInstruction(final int aOpcode, final Label aLabel) {
        return new JumpInstructionBytecodeEventImpl(aOpcode, aLabel);
    }

    @Override
    public LoadConstantInstructionBytecodeEvent createLoadConstantInstruction(final Object aConstant) {
        final int hashCode = hashCode(BytecodeEvent.LOAD_CONSTANT_INSTRUCTION, aConstant.hashCode());
        if (cachedBytecodeEvents.containsKey(hashCode)) {
            return (LoadConstantInstructionBytecodeEvent) cachedBytecodeEvents.get(hashCode);
        }

        final LoadConstantInstructionBytecodeEvent loadConstantInstructionBytecodeEvent = new LoadConstantInstructionBytecodeEventImpl(
            aConstant);
        cachedBytecodeEvents.put(hashCode, loadConstantInstructionBytecodeEvent);
        return loadConstantInstructionBytecodeEvent;
    }

    @Override
    public LookupSwitchInstructionBytecodeEvent createLookupSwitchInstruction(final Label aDefaultLabel,
        final int[] aKeys, final Label[] aBlockLabels)
    {
        return new LookupSwitchInstructionBytecodeEventImpl(aDefaultLabel, aKeys, aBlockLabels);
    }

    @Override
    public MethodInstructionBytecodeEvent createMethodInstruction(final int aOpcode, final String aOwnerName,
        final String aMethodName, final String aMethodDescriptor)
    {
        return new MethodInstructionBytecodeEventImpl(aOpcode, aOwnerName, aMethodName, aMethodDescriptor);
    }

    @Override
    public MultiANewArrayInstructionBytecodeEvent createMultiANewArrayInstruction(final String aComponentDescriptor,
        final int aDimensions)
    {
        return new MultiANewArrayInstructionBytecodeEventImpl(aComponentDescriptor, aDimensions);
    }

    @Override
    public TableSwitchInstructionBytecodeEvent createTableSwitchInstruction(final int aMinimumKeyValue,
        final int aMaximumKeyValue, final Label aDefaultLabel, final Label[] aBlockLabels)
    {
        return new TableSwitchInstructionBytecodeEventImpl(aMinimumKeyValue, aMaximumKeyValue, aDefaultLabel,
            aBlockLabels);
    }

    @Override
    public TypeInstructionBytecodeEvent createTypeInstruction(final int aOpcode, final String aTypeName) {
        final int hashCode = hashCode(BytecodeEvent.TYPE_INSTRUCTION, aOpcode, aTypeName.hashCode());
        if (cachedBytecodeEvents.containsKey(hashCode)) {
            return (TypeInstructionBytecodeEvent) cachedBytecodeEvents.get(hashCode);
        }

        final TypeInstructionBytecodeEvent typeInstructionBytecodeEvent = new TypeInstructionBytecodeEventImpl(aOpcode,
            aTypeName);
        cachedBytecodeEvents.put(hashCode, typeInstructionBytecodeEvent);
        return typeInstructionBytecodeEvent;
    }

    @Override
    public VariableInstructionBytecodeEvent createVariableInstruction(final int aOpcode, final int aLocalVariableIndex)
    {
        final int hashCode = hashCode(BytecodeEvent.VARIABLE_INSTRUCTION, aOpcode, aLocalVariableIndex);
        if (cachedBytecodeEvents.containsKey(hashCode)) {
            return (VariableInstructionBytecodeEvent) cachedBytecodeEvents.get(hashCode);
        }

        final VariableInstructionBytecodeEvent variableInstructionBytecodeEvent = new VariableInstructionBytecodeEventImpl(
            aOpcode, aLocalVariableIndex);
        cachedBytecodeEvents.put(hashCode, variableInstructionBytecodeEvent);
        return variableInstructionBytecodeEvent;
    }

    @Override
    public LabelBytecodeEvent createLabel(final Label aLabel) {
        return new LabelBytecodeEventImpl(aLabel);
    }

    @Override
    public TryCatchBlockBytecodeEvent createTryCatchBlock(final Label aStartLabel, final Label aEndLabel,
        final Label aHandlerLabel, final String aExceptionName)
    {
        return new TryCatchBlockBytecodeEventImpl(aStartLabel, aEndLabel, aHandlerLabel, aExceptionName);
    }

    @Override
    public LocalVariableBytecodeEvent
        createLocalVariable(final String aName, final String aDescriptor, final String aSignature,
            final Label aStartScopeLabel, final Label aEndScopeLabel, final int aLocalVariableIndex)
    {
        return new LocalVariableBytecodeEventImpl(aName, aDescriptor, aSignature, aStartScopeLabel, aEndScopeLabel,
            aLocalVariableIndex);
    }

    @Override
    public LineNumberBytecodeEvent createLineNumber(final int aLineNumber, final Label aStartLabel) {
        return new LineNumberBytecodeEventImpl(aLineNumber, aStartLabel);
    }

    @Override
    public CodeEndBytecodeEvent createCodeEnd(final int aMaximumStackSize, final int aMaximumLocalVariables) {
        final int hashCode = hashCode(BytecodeEvent.CODE_END, aMaximumStackSize, aMaximumLocalVariables);
        if (cachedBytecodeEvents.containsKey(hashCode)) {
            return (CodeEndBytecodeEvent) cachedBytecodeEvents.get(hashCode);
        }

        final CodeEndBytecodeEvent codeEndBytecodeEvent = new CodeEndBytecodeEventImpl(aMaximumStackSize,
            aMaximumLocalVariables);
        cachedBytecodeEvents.put(hashCode, codeEndBytecodeEvent);
        return codeEndBytecodeEvent;
    }

    @Override
    public MethodEndBytecodeEvent createMethodEnd() {
        return MethodEndBytecodeEventImpl.getInstance();
    }

    @Override
    public ClassEndBytecodeEvent createClassEnd() {
        return ClassEndBytecodeEventImpl.getInstance();
    }

    private static int hashCode(final int aFirst, final int aSecond) {
        return HASH_PRIME * (HASH_PRIME + aFirst) + aSecond;
    }

    private static int hashCode(final int aFirst, final int aSecond, final int aThird) {
        return HASH_PRIME * hashCode(aFirst, aSecond) + aThird;
    }
}
